﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using Quail.Helpers;
using Quail.Parallel;
using QuailServices;

namespace Quail
{
    public class PerformanceTestConfig
    {
        private readonly ITestShell _shell;
        private string _baseUrl;

        ///<summary>
        /// Creates a new instance of the PerformanceTestConfig object
        ///</summary>
        ///<param name="shell">The test shell</param>
        ///<param name="createNewCaptureTestShell">Delegate to create a new test shell</param>
        public PerformanceTestConfig(ITestShell shell, Func<int, ITestShell> createNewCaptureTestShell = null)
            : this(shell, null, createNewCaptureTestShell)
        {
        }

        ///<summary>
        /// Creates a new instance of the PerformanceTestConfig object
        ///</summary>
        ///<param name="shell">The test shell</param>
        ///<param name="urls">The urls to process</param>
        /// <param name="createNewCaptureTestShell">Delegate to create a new test shell</param>
        public PerformanceTestConfig(ITestShell shell, IEnumerable<string> urls, Func<int, ITestShell> createNewCaptureTestShell = null)
        {
            _shell = shell;
            Urls = urls == null ? null : urls.ToList();
            CreateNewCaptureTestShell = createNewCaptureTestShell;
            CaptureTraffic = false;
            NumberOfTestShells = 1;
            NumberOfTests = 5;
            BrowserTestType = BrowserTestTypes.HttpAndSelenium;
        }

        /// <summary>
        /// Whether to capture all load time data
        /// </summary>
        public bool CaptureTraffic { get; set; }

        public enum BrowserTestTypes
        {
            HttpAndSelenium,
            HttpGetOnly,
            SeleniumOnly
        } 

        public BrowserTestTypes BrowserTestType { get; set; }

        /// <summary>
        /// Whether to execute only HttpGet requests
        /// </summary>
        public bool HttpGetOnly
        {
            get { return BrowserTestType == BrowserTestTypes.HttpGetOnly; }
        }

        /// <summary>
        /// Whether to execute only BrowserTestOnly requests
        /// </summary>
        public bool BrowserTestOnly
        {
            get { return BrowserTestType == BrowserTestTypes.SeleniumOnly; }
        }

        /// <summary>
        /// Number of times to test each url
        /// </summary>
        public int NumberOfTests { get; set; }

        /// <summary>
        /// The number of test shells to open. 
        /// </summary>
        public int NumberOfTestShells { get; set; }

        /// <summary>
        /// The shell your test opened in
        /// </summary>
        public ITestShell Shell { get { return _shell; } }

        /// <summary>
        /// A list of urls to test
        /// </summary>
        public List<string> Urls { get; set; }

        /// <summary>
        /// Provide a base url to override the shell's base url
        /// </summary>
        public string BaseUrl
        {
            get
            {
                return _baseUrl ?? ComputeRootUrl(Shell);
            }

            set
            {
                _baseUrl = value;
            }
        }

        /// <summary>
        /// Delegate to create a new test shell
        /// </summary>
        public Func<int, ITestShell> CreateNewCaptureTestShell { get; set; }


        private static string ComputeRootUrl(ITestShell shell)
        {
            var uri = new Uri(shell.TestSession.BaseUrl);

            return "http://" + uri.Host +
                   (uri.IsDefaultPort ? string.Empty : ":" + uri.Port);
        }
    }

    public static class PerformanceTestHelper
    {
        /// <summary>
        /// Executes a standard load test
        /// </summary>
        /// <param name="testname">The name of the test method</param>
        /// <param name="owninginstance">The owning instance (you should probably pass: this)</param>
        /// <param name="datagroup">The load test data group</param>
        /// <param name="environment">The environment</param>
        /// <param name="baseurl">The base url of the tests</param>
        /// <param name="numberoftests">How many tests of each URL to perform (default should be 300)</param>
        /// <param name="numberofthreads">How many threads to use for the test (default should be 3)</param>
        public static void StandardLoadTestRun(string testname, object owninginstance, string datagroup, string environment, string baseurl, int numberoftests, int numberofthreads)
        {
            using (var shell = EmptyShell.Start(testname, owninginstance, environment, ""))
            {
                var task = shell.ActiveTask.BeginTask(testname);
                try
                {
                    var ltd = GetLoadTestData(datagroup);

                    RunLoadTest(ltd, new PerformanceTestConfig(shell)
                                         {
                                             BaseUrl =
                                                 baseurl,
                                             BrowserTestType = PerformanceTestConfig.BrowserTestTypes.HttpGetOnly,
                                             NumberOfTestShells = numberofthreads,
                                             NumberOfTests = numberoftests
                                         });

                    shell.TestSession.RootTask.AddLog(LogStatus.Information,
                                                      string.Format(
                                                          "http://qaquailweb/LoadTest.aspx?bytests=1&group={0}&env={1}",
                                                          datagroup, environment));
                    task.Finish();
                }
                catch (Exception e)
                {
                    task.Finish(e);
                }
            }
        }

        public static List<QuailServices.QuailDataServiceReference.LoadTestData> GetLoadTestData(string group)
        {
            using (var client = QuailDataServiceFactory.CreateClient())
            {
                var result = client.GetLoadTestData(group);

                return result.ToList();
            }
        }

        /// <summary>
        /// Runs a load test using a specific list of test data from the LoadTestData table
        /// </summary>
        /// <param name="testData"></param>
        /// <param name="config"></param>
        public static void RunLoadTest(List<QuailServices.QuailDataServiceReference.LoadTestData> testData, PerformanceTestConfig config)
        {
            var shell = config.Shell;
            var task = shell.ActiveTask.BeginTask("RunLoadTest");

            task.AddLog(LogStatus.Information, "Using " + config.NumberOfTests + " tests and " + config.NumberOfTestShells + " threads.");
            try
            {
                if (config.NumberOfTestShells > 100)
                    throw new Exception("Too many threads in load test.  Please be reasonable and limit yourself to no more than 100.");

                var queue = new ConcurrentQueue<LoadTestContext>();
                var baseurl = config.BaseUrl;

                var tds = testData
                    .Select(td => new {td, isabsolute = new Uri(td.Url, UriKind.RelativeOrAbsolute).IsAbsoluteUri})
                    .Select(td =>
                            new
                                {
                                    td.td,
                                    url = td.isabsolute
                                              ? td.td.Url
                                              : baseurl +
                                                (td.td.Url.StartsWith("/") ? td.td.Url : "/" + td.td.Url)
                                })
                    .ToList();

                config.Urls = tds.Select(td => td.url).ToList();

                // queue up the test urls
                var list = new List<LoadTestContext>();

                // add all the tests
                list
                    .AddRange(tds
                                    .Select(td => new LoadTestContext
                                                    {
                                                        Context = td.td,
                                                        HttpRequest = true,
                                                        Url = td.url
                                                    }));
                WarmupLoadTest(config, list);

                var biglist = new List<LoadTestContext>();
                for (var i = 0; i < config.NumberOfTests; i++)
                    biglist.AddRange(list);

                foreach (var ltc in biglist.RandomItems())
                    queue.Enqueue(ltc);

                biglist.Clear();
                biglist = null;

                var timpl = task.TaskImplementors.FirstOrDefault(ti => ti is ITestRunId) as ITestRunId;

                if (timpl == null)
                    throw new Exception("No Task Implementor found that implements ITestRunId, unable to run LoadTest");

                using (var client = QuailDataServiceFactory.CreateClient())
                {
                    var testrunid = timpl.GetTestRunId(5*60*1000);

                    var loadTestId = client.CreateLoadTest(testrunid, null, config.NumberOfTests, config.NumberOfTestShells);
                    RunLoadTest(config, queue);

                    foreach (var url in list)
                    {
                        var ltd = (QuailServices.QuailDataServiceReference.LoadTestData) url.Context;
                        var times = url.GetValues();
                        client.AddLoadTestUrlRun(loadTestId,
                                                 ltd.LoadTestDataId,
                                                 times.Select(t => (int) t.FirstByteMs).ToArray(),
                                                 times.Select(t => (int) t.TotalMs).ToArray(),
                                                 times.Select(t => t.Size).ToArray(),
                                                 times.Select(t => t.StatusCode).ToArray(),
                                                 times.Select(t => t.StartTime).ToArray());
                        ;
                    }

                    client.FinishLoadTest(loadTestId);
                }

                task.Finish();
            }
            catch (Exception ex)
            {
                if (task != null)
                    task.Finish(ex);
                throw;
            }

        }

        /// <summary>
        /// Warms up the web site
        /// </summary>
        /// <param name="config"></param>
        /// <param name="list"></param>
        private static void WarmupLoadTest(PerformanceTestConfig config, IEnumerable<LoadTestContext> list)
        {
            var task = config.Shell.ActiveTask.BeginTask("WarmupLoadTest");
            try
            {
                var data = new byte[1000000];
                var sw = new Stopwatch();
                var enc = new ASCIIEncoding();
                string rawdata;

                foreach (var loadcontext in list)
                {
                    if (loadcontext.HttpRequest)
                    {
                        bool failed;
                        TimeDetail td;

                        GetResponseTimeToFirstByte(loadcontext.Url, data, sw,
                                                   out td, out failed);

                        rawdata = enc.GetString(data, 0, td.Size);
                    }
                    else
                    {
                        try
                        {
                            config.Shell.TestSession.Browser.NavigateWithTimeoutRetry(loadcontext.Url);
                        }
                        catch (Exception ex)
                        {
                            // absorb exceptions?
                        }
                    }
                }
                task.Finish(task.OverallStatus.IsFailingStatus()
                                ? LogStatus.Warning
                                : task.OverallStatus);
            }
            catch (Exception ex)
            {
                task.Finish(ex);
                throw;
            }
        }

        /// <summary>
        /// Runs a load test against a set of web addresses using a configuration
        /// </summary>
        /// <param name="config">The configuration</param>
        public static void RunTestAgainstUrls(PerformanceTestConfig config)
        {
            var shell = config.Shell;
            var task = shell.ActiveTask.BeginTask("RunTestAgainstUrls");
            try
            {
                // var dict = new Dictionary<string, LoadTestContext>();

                var queue = new ConcurrentQueue<LoadTestContext>();

                var baseurl = config.BaseUrl;

                var tests = new List<LoadTestDetail>();

                foreach (var url in config.Urls)
                {
                    var uri = new Uri(url, UriKind.RelativeOrAbsolute);
                    var completeUrl =
                        uri.IsAbsoluteUri
                            ? url
                            : baseurl + (url.StartsWith("/") ? url : "/" + url);


                    var test = new LoadTestDetail();

                    // when the load test is not only HTTP GET then:
                    if (!config.HttpGetOnly)
                    {
                        // add a LoadTestContext for a Selenium test - url can be relative or absolute
                        test.Selenium = new LoadTestContext { Url = url, UrlDisplay = completeUrl };
                    }


                    if (!config.BrowserTestOnly)
                    {
                        test.HttpGet = new LoadTestContext { Url = completeUrl, UrlDisplay = completeUrl, HttpRequest = true };
                    }

                    tests.Add(test);
                }

                var alltests = tests
                    .Where(t => t.HttpGet != null)
                    .Select(t => t.HttpGet)
                    .Union(tests
                               .Where(t => t.Selenium != null)
                               .Select(t => t.Selenium))
                    .ToList();

                for (var i = 0; i < config.NumberOfTests; i++)
                {
                    alltests
                        .RandomItems()
                        .ToList()
                        .ForEach(queue.Enqueue);
                }

                WarmupLoadTest(config, alltests);
                RunLoadTest(config, queue);

                var tableresults =
                    "<table><tr><td>Total Tests</td><td>URL</td><td>Avg Load (ms)</td><td>Avg Request (ms)</td><td>Min</td><td>Max</td><td>Median</td></tr>";

                const string formatrow =
                    "<tr><td>{0}</td><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td><td>{5}</td><td>{6}</td></tr>";

                foreach (var test in tests)
                {
                    var loadcontext = test.Selenium ?? test.HttpGet;
                    var httpResult = test.HttpGet ?? test.Selenium;

                    tableresults += string.Format(formatrow,
                                                    loadcontext.TotalTests,
                                                    loadcontext.UrlDisplay,
                                                    loadcontext.AvgTime,
                                                    httpResult.AvgTime,
                                                    loadcontext.MinTime,
                                                    loadcontext.MaxTime,
                                                    loadcontext.Median);
                }

                tableresults += "</table>";

                shell.TestSession.RootTask.AddLog(tableresults);

                task.Finish();
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }
        }

        private class LoadTestDetail
        {
            public LoadTestContext Selenium { get; set; }
            public LoadTestContext HttpGet { get; set; }
        }

        private static void RunLoadTest(PerformanceTestConfig config, ConcurrentQueue<LoadTestContext> queue)
        {
            if (config.HttpGetOnly)
            {
                var workers = new List<Thread>();

                for (var i = 0; i < config.NumberOfTestShells; i++)
                {
                    var worker =
                        new Thread(
                            () =>
                            {
                                var data = new byte[32000];
                                var sw = new Stopwatch();

                                for (; ; )
                                {
                                    LoadTestContext loadcontext;
                                    if (!queue.TryDequeue(out loadcontext))
                                        return;

                                    bool failed;
                                    TimeDetail td;

                                    GetResponseTimeToFirstByte(loadcontext.Url, data, sw,
                                                               out td, out failed);
                                    loadcontext.AddData(td);
                                }
                            });

                    workers.Add(worker);
                }

                var subtask = config.Shell.ActiveTask.BeginTask("Running worker threads");
                try
                {
                    var sw2 = new Stopwatch();
                    var total = queue.Count;
                    sw2.Start();

                    workers.ForEach(w => w.Start());

                    foreach (var url in config.Urls)
                    {
                        subtask.AddLog(LogStatus.Information, url);
                    }

                    // wait for the threads
                    workers.ForEach(w => w.Join());
                    sw2.Stop();
                    subtask.AddLog(LogStatus.Information,
                                   "Opened " + total + " urls, total test time: " + sw2.ElapsedMilliseconds);
                    subtask.Finish();
                }
                catch (Exception e)
                {
                    subtask.Finish(e);
                    throw;
                }

            }
            else
            {
                using (var testManager = new ParallelTestManager<ITestShell, LoadTestContext>(config.Shell, config.NumberOfTestShells))
                {
                    testManager
                        .GetNextWorkItemMethod =
                        context =>
                        {
                            LoadTestContext ctx;
                            return queue.TryDequeue(out ctx) ? ctx : null;
                        };

                    if (config.CaptureTraffic)
                    {
                        testManager.CreateParallelTestShell = config.CreateNewCaptureTestShell;
                    }

                    testManager.TestMethod = RunTest;

                    testManager.RunTests();
                }
            }            
        }

        private static void RunTest(ITestShell shell, LoadTestContext loadcontext)
        {
            PerformanceTestRunState state;
            try
            {
                state = shell.InstanceState as PerformanceTestRunState;
                if (state == null)
                {
                    // pre-init the active task to avoid some CPU overhead
                    state = new PerformanceTestRunState { ActiveTask = shell.ActiveTask };
                    shell.InstanceState = state;
                }
            }
            catch (Exception)
            {
                shell.TestSession.RootTask.AddLog(LogStatus.Fail, "failed when setting performance state");
                throw;
            }

            var task = state.ActiveTask.BeginTask(String.Format("{0} - {1}", loadcontext.Url, loadcontext.HttpRequest ? "httpget" : "selenium"));
            try
            {
                long totalms;
                if (loadcontext.HttpRequest)
                {
                    totalms = Http.GetResponseTime(loadcontext.Url);
                }
                else
                {
                    var sw = state.Sw;
                    sw.Reset();

                    if (shell.TestSession.CaptureNetworkTraffic)
                        shell.TestSession.Browser.NetworkTrafficLogger.ClearTraffic();

                    sw.Start();

                    shell.TestSession.Browser.Navigate(loadcontext.Url);
                    
                    // TESTING TO PROVE IT WAITS FOR DELAY LADED FILES
                    //shell.Browser.SeleniumInterface.Open(loadcontext.Url);
                    //var traffic = shell.Browser.NetworkTrafficLogger.GetTraffic();
                    //traffic = traffic;
                    //shell.Browser.Sleep(1500);
                    //var traffic2 = shell.Browser.NetworkTrafficLogger.GetTraffic();
                    //traffic2 = traffic2;
                    
                    sw.Stop();

                    shell.TestSession.Browser.Sleep(1500);

                    if (shell.TestSession.CaptureNetworkTraffic)
                        shell.TestSession.Browser.NetworkTrafficLogger.LogTraffic((int)sw.ElapsedMilliseconds, loadcontext.Url);

                    totalms = sw.ElapsedMilliseconds;
                }

                loadcontext.AddData(totalms);

                //if (!loadcontext.HttpRequest)
                //    shell.Browser.Navigate("/");

                task.Finish();
            }
            catch (YSODFoundException ysod)
            {
                // clear the YSOD page
                shell.TestSession.Browser.Navigate("/");
                task.Finish(ysod);
            }
            catch (Exception e)
            {

                task.Finish(e);
            }
        }


        /// <summary>
        /// Opens the URL and returns the time it took in ms
        /// </summary>
        /// <param name="url">The URL.</param>
        /// <param name="data">The data array to avoid memory overhead</param>
        /// <param name="sw">the stop watch</param>
        /// <param name="td">The output time detail</param>
        /// <param name="failed">True if the request failed</param>
        /// <returns></returns>
        private static void GetResponseTimeToFirstByte(string url, byte[] data, Stopwatch sw, out TimeDetail td, out bool failed)
        {
            var req = WebRequest.Create(url);
            req.Timeout = 30000;

            td = new TimeDetail {StartTime = DateTime.Now};
            sw.Restart();
            try
            {
                var offset = 0;
                req.Headers.Add("cache-control", "no-cache");
                using (var res = req.GetResponse())
                {
                    td.StatusCode = 200; // assume 200?
                    using (var str = res.GetResponseStream())
                    {
                        var got = str.Read(data, offset, 1);
                        td.FirstByteMs = sw.ElapsedMilliseconds;
                        td.Size = got;
                        offset += got;

                        if (got > 0)
                        {
                            var l = data.Length - got;
                            // read the rest of the content
                            for (;;)
                            {
                                if (offset >= data.Length)
                                {
                                    offset = 0;
                                    l = data.Length;
                                }

                                if ((got = str.Read(data, offset, l)) == 0)
                                    break;

                                l -= got;
                                offset += got;

                                td.Size += got;
                            }
                        }
                    }
                }
                
                failed = false;
            }
            catch (WebException ex)
            {
                // absorb ex
                if (ex.Status == WebExceptionStatus.ProtocolError)
                {
                    var hwr = ex.Response as HttpWebResponse;
                    if (hwr != null)
                        td.StatusCode = (int)hwr.StatusCode;
                }
                else if (ex.Status == WebExceptionStatus.Timeout)
                    td.StatusCode = 408;
                else
                    td.StatusCode = 666;

                failed = true;
            }

            td.TotalMs = sw.ElapsedMilliseconds;
            sw.Stop();
        }

        public class PerformanceTestRunState
        {
            public PerformanceTestRunState()
            {
                Sw = new Stopwatch();
            }

            /// <summary>
            /// Gets or sets the active task.
            /// </summary>
            /// <value>The active task.</value>
            public Task ActiveTask
            {
                get;
                set;
            }

            /// <summary>
            /// Gets the stopwatch
            /// </summary>
            public Stopwatch Sw
            {
                get;
                private set;
            }
        }

        private class TimeDetail
        {
            public long TotalMs { get; set; }
            public long FirstByteMs { get; set; }
            public int Size { get; set; }
            public int StatusCode { get; set; }
            public DateTime? StartTime { get; set; }
        }


        /// <summary>
        /// Used during Browse load tests
        /// </summary>
        private class LoadTestContext
        {
            private readonly ConcurrentBag<TimeDetail> _times = new ConcurrentBag<TimeDetail>();

            /// <summary>
            /// Gets the total time spent during the test
            /// </summary>
            public long TotalTime
            {
                get
                {
                    lock (this)
                    {
                        return _times
                            .Select(t => t.TotalMs)
                            .Aggregate((s, i) => s + i);
                    }
                }
            }

            public List<TimeDetail> GetValues()
            {
                lock (this)
                {
                    return _times
                        .ToList();
                }
            }

            /// <summary>
            /// Gets the median time in the test
            /// </summary>
            public long Median
            {
                get
                {
                    var data = _times
                        .Select(t => t.TotalMs)
                        .ToList()
                        .OrderBy(i => i)
                        .ToList();

                    if (data.Count == 0)
                        return 0;

                    return data[data.Count/2];
                }
            }

            /// <summary>
            /// Gets the minimum time spent during the test
            /// </summary>
            public long MinTime
            {
                get
                {
                    return _times.Count > 0
                               ? _times.Min(t => t.TotalMs)
                               : 0;
                }
            }

            /// <summary>
            /// Gets the maximum time spent during the test
            /// </summary>
            public long MaxTime
            {
                get
                {
                    return _times.Count > 0
                               ? _times.Max(t => t.TotalMs)
                               : 0;
                }
            }

            /// <summary>
            /// Gets number of tests executed
            /// </summary>
            public long TotalTests
            {
                get
                {
                    lock (this)
                    {
                        return _times.Count;
                    }
                }
            }

            /// <summary>
            /// Gets average time used
            /// </summary>
            public long AvgTime
            {
                get
                {
                    lock (this)
                    {
                        return TotalTests > 0 ? TotalTime/TotalTests : 0;
                    }
                }
            }

            public string UrlDisplay
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets the url to open
            /// </summary>
            public string Url
            {
                get;
                set;
            }

            /// <summary>
            /// Gets or sets a value indicating whether to use Http Request (instead of Selenium)
            /// </summary>
            public bool HttpRequest
            {
                get;
                set;
            }


            /// <summary>
            /// Gets or sets a custom context value for this test context
            /// </summary>
            public object Context { get; set; }

            /// <summary>
            /// Adds the time to the TotalTime and count to the total count
            /// </summary>
            /// <param name="time">time spent in the test</param>
            public void AddData(long time)
            {
                lock (this)
                    _times.Add(new TimeDetail {TotalMs = time});
            }
            /// <summary>
            /// Adds the time to the TotalTime and count to the total count
            /// </summary>
            /// <param name="td">time spent in the test</param>
            public void AddData(TimeDetail td)
            {
                lock (this)
                {
                    _times.Add(td);
                }
            }
        }
    }
}
